/*
 * 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙，邂逅框架梦，本文采用木兰宽松许可证第2版]
 * 
 * https://zhiqim.org/project/zhiqim_kit/zhiqim_git.htm
 *
 * ZhiqimGit is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.zhiqim.git.command;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.zhiqim.git.Git;
import org.zhiqim.git.GitCommand;
import org.zhiqim.git.GitConstants;
import org.zhiqim.git.GitWalker;
import org.zhiqim.git.http.Progress;
import org.zhiqim.git.http.ProgressEmpty;
import org.zhiqim.git.http.ReceiveCommandType;
import org.zhiqim.git.http.UploadWriter;
import org.zhiqim.git.http.model.Advertiser;
import org.zhiqim.git.http.model.Push;
import org.zhiqim.git.http.model.PushResult;
import org.zhiqim.git.objer.objectid.ObjectId;
import org.zhiqim.git.objer.objectid.RevCommit;
import org.zhiqim.git.objer.objectid.RevObject;
import org.zhiqim.git.objer.objectid.RevTag;
import org.zhiqim.git.refer.Ref;
import org.zhiqim.git.util.GitHttps;
import org.zhiqim.git.util.GitStreams;
import org.zhiqim.kernel.constants.HttpConstants;
import org.zhiqim.kernel.extend.HashMapSV;
import org.zhiqim.kernel.extend.MapSV;
import org.zhiqim.kernel.httpclient.HttpStream;
import org.zhiqim.kernel.util.Strings;

/**
 * 推送到服务端
 * http://www.kernel.org/pub/software/scm/git/docs/git-push.html
 *
 * @version v1.4.1 @author zouzhigang 2018-8-2 新建与整理
 */
public class PushCommand extends GitCommand<PushResult> implements HttpConstants, GitConstants
{
    private GitWalker walker;
    private String remoteUrl;
    private int dept;
    
    private String userName;
    private String userPass;
    
    //客户端已有列表 & 要求的引用对象列表(标签) & 要求的提交列表
    private final Set<RevCommit> haveCommitList = new HashSet<>();
    private final LinkedList<RevCommit> wantCommitList = new LinkedList<>();
    private final List<RevObject> wantRefobjList = new ArrayList<>();
    
    public PushCommand(Git git)
    {
        super(git);
        
        this.walker = new GitWalker(git);
        this.remoteUrl = Strings.addEndsWith(git.getConfig().getString("remote", "origin", "url"), "/");
        this.dept = 2;
    }
    
    public PushCommand setNamePass(String userName, String userPass)
    {
        this.userName = userName;
        this.userPass = userPass;
        return this;
    }
    
    public PushCommand setRemoteUrl(String remoteUrl)
    {
        this.remoteUrl = Strings.addEndsWith(remoteUrl, "/");
        return this;
    }

    public PushCommand setDept(int dept)
    {
        this.dept = dept;
        return this;
    }
    
    @Override
    protected PushResult execute() throws Exception
    {
        //第一步，先info/refs，拿到远程的引用列表
        InfoRefsCommand command = new InfoRefsCommand(git)
            .setRemoteUrl(remoteUrl)
            .setNamePass(userName, userPass)
            .setService(RECEIVE_PACK);
        
        Advertiser advertiser = command.call();
        MapSV<Ref> remoteMap = advertiser.getRefList();
        
        //第二步，读取本地分枝引用列表，进行比较
        Map<String, Ref> refMap = git.getRefMap(P_REFS_HEADS_);
        refMap.putAll(git.getRefMap(P_REFS_TAGS_));
        
        //第三步，比较本地和远程引用，得到PushResult
        MapSV<Push> pushMap = new HashMapSV<>();
        for (Iterator<Ref> it=refMap.values().iterator();it.hasNext();)
        {
            Ref localRef = it.next();
            String localName = localRef.getName();
            RevCommit localCommit = walker.parseCommit(localRef.getObjectId());
            
            Ref remoteRef = remoteMap.get(localRef.getName());
            if (remoteRef == null)
            {//本地存在，远程不存在的，创建
                
                RevCommit deptCommit = walker.getFastward(localCommit, dept);
                pushMap.put(localName, new Push().setRef(localRef).setType(ReceiveCommandType.CREATE).setLocalRemoteId(deptCommit, ZEROID));
                
                addWant(deptCommit);
                continue;
            }
            
            RevCommit remoteCommit = walker.parseCommitNullable(remoteRef.getObjectId());
            if (remoteCommit == null)
            {//本地没有远程的提交
                throw new IOException("远程引用不在本地，请先pull："+remoteCommit);
            }
            
            if (localCommit.equals(remoteCommit))
            {//相同的不需要处理
                addHave(remoteCommit);
                continue;
            }
            
            RevCommit deptCommit = walker.getFastward(remoteCommit, localCommit, dept);
            if (deptCommit == null)
            {//本地不是在远程基础上增加的提交，要求pull
                throw new IOException("远程引用提交不是本地提交的父提交，请先pull："+remoteCommit);
            }
            
            pushMap.put(localName, new Push().setRef(localRef).setType(ReceiveCommandType.UPDATE).setLocalRemoteId(deptCommit, remoteCommit));
            
            addHave(remoteCommit);
            addWant(deptCommit);
        }
        
        for (Iterator<Ref> it=remoteMap.values().iterator();it.hasNext();)
        {
            Ref remoteRef = it.next();
            ObjectId remoteOid = remoteRef.getObjectId();
            if (remoteOid.equals(ZEROID))
            {//服务端是空的
                continue;
            }
            
            if (!refMap.containsKey(remoteRef.getName()))
            {
                pushMap.put(remoteRef.getName(), new Push().setRef(remoteRef).setType(ReceiveCommandType.DELETE).setLocalRemoteId(ZEROID, remoteRef.getObjectId()));
                addWant(remoteRef.getObjectId());
            }
        }
        
        if (pushMap.isEmpty())
        {//不需要同步
            return new PushResult().setSuccess(true).setDesc("和服务端相同").setPushMap(pushMap);
        }
        
        //第四步，组装功能和命令数据
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        boolean first = true;
        for (Push push : pushMap.values())
        {
            String remoteId = push.getRemoteId().getName();
            String localId = push.getLocalId().getName();
            
            StringBuilder strb = new StringBuilder(remoteId).append(_SPACE_).append(localId).append(_SPACE_).append(push.getRef().getName());
            if (first)
            {
                first = false;
                strb.append(_NUL_).append(OPTION_REPORT_STATUS);
            }
            
            GitStreams.writeString(out, strb.toString());
        }
        GitStreams.writeEnd(out);
        
        //第五步，组装对象数据
        Progress progress = ProgressEmpty.getInstance();
        UploadWriter pw = new UploadWriter(git, walker, progress);
        pw.preparePack(wantRefobjList, wantCommitList, haveCommitList);
        pw.writePack(out);
        
        //第五步，PUSH到服务端UPLOAD_PACK_REQUEST_TYPE
        HttpStream conn = new HttpStream(remoteUrl + RECEIVE_PACK, _POST_);
        conn.addRequestProperty(_USER_AGENT_, ZHIQIM_GIT_VERSION);
        conn.addRequestProperty(_CONTENT_TYPE_, RECEIVE_PACK_REQUEST_TYPE);
        conn.addRequestProperty(_PRAGMA_, _NO_CACHE_);
        conn.addRequestProperty(_CACHE_CONTROL_, _NO_CACHE_MUST_RV_NO_STORE_);
        conn.addRequestProperty(_ACCEPT_, RECEIVE_PACK_RESULT_TYPE);
        conn.addRequestProperty(_AUTHORIZATION_, GitHttps.authorization(userName, userPass));
        conn.write(out.toByteArray());
        conn.execute();
        
        if (!conn.isResponseSuccess())
        {
            return new PushResult().setSuccess(false).setDesc(conn.getResponseText()).setPushMap(pushMap);
        }
        

        PushResult result = new PushResult().setPushMap(pushMap);
        InputStream in = conn.getInputStream();
        boolean isFirst = true;
        while (true)
        {
            String line = GitStreams.readString(in);
            if (line == null)
                break;
            
            if (isFirst)
            {
                isFirst = false;
                line = line.substring("unpack ".length());
                if ("ok".equals(line))
                    result.setSuccess(true).setDesc(line);
                else
                    result.setSuccess(false).setDesc(line);
            }
            else
            {
                String refName = line.split(" ")[1];
                pushMap.get(refName).setResult(line);
            }
        }
        
        return result;
    }

    private void addWant(ObjectId oid) throws IOException
    {
        RevObject obj = walker.parse(oid);
        if (obj.isWant())
            return;
        
        obj.setWant();
        if (obj instanceof RevCommit)
            wantCommitList.add((RevCommit)obj);
        else
            wantRefobjList.add(obj);
        
        if (obj instanceof RevTag)
        {//如果是标签，找到对应的提交
            obj = walker.peel(obj);
            if (obj instanceof RevCommit)
                addWant(obj);
        }
    }
    
    private boolean addHave(ObjectId oid) throws IOException
    {
        RevCommit commit = walker.parseCommitNullable(oid);
        if (commit == null)
        {//对象不在服务端，返回false表示不需要回ACK
            return false;
        }
        
        if (commit.isHave())
        {//对象存在，且已标识在父提交中，返回true表示要回ACK
            return true;
        }
        
        //递归设置have标志到所有父提交中
        commit.setHave();
        commit.setHaveParentRecursion(walker, haveCommitList);
        haveCommitList.add(commit);
        
        //对象存在服务端，返回true表示要回ACK
        return true;
    }
}
